package org.fujene.communicate;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.logging.Level;
import org.fujene.communicate.node.Node;
import org.fujene.constants.Constants;
import org.fujene.logging.LogParser;
import org.fujene.structure.ContentInfo;
import org.fujene.term.BigramChopper;
import org.fujene.term.Chopper;
import org.fujene.term.WordBigramChopper;
import org.msgpack.MessagePackObject;
import org.msgpack.object.ArrayType;
import org.msgpack.object.IntegerType;
import org.msgpack.object.RawType;
import org.msgpack.rpc.error.ArgumentError;

public class MainServiceSettings{
	public static final int DELTYPE_ID = Constants.DELETE_TYPE_ID;
	public static final int DELTYPE_CONTENT = Constants.DELETE_TYPE_CONTENT;
	public static final int DELTYPE_APPENDIX = Constants.DELETE_TYPE_APPENDIX;
	public static final int DELTYPE_MULTIFLAG = Constants.DELETE_TYPE_MULTIFLAG;
	public static final int DELTYPE_TIME = Constants.DELETE_TYPE_TIME;

	private int indexThreadNum;
	private int searchThreadNum;

	private String directory;
	private int blockNum;
	private int blockSize;

	private int peerport;
	private int indexport;
	private int searchport;

	private int replication;
	private int virtualNodes;
	private int rankType;
	private int beaconInterval;
	private int beaconThreshold;
	private int backupInterval;
	private int deleteKeyType;
	private int deleteKeyIndex;
	private int uniqueKeyIndex;

	private SortedSet<Node> nodeList;
	private Node myNode;
	private String[] firstHost;
	private Chopper chopper;
	private ContentInfo contentInfo;
	private ConcurrentSkipListMap<Long, Node> hashList;

	private boolean clientLog = false;
	private boolean isRerank = false;

	private MainServiceSettings(){}

	public static MainServiceSettings initWithDefault(){
		MainServiceSettings setting = new MainServiceSettings();
		setting.blockNum = 256;
		setting.blockSize = 1024 * 1024;
		setting.directory = "./orbis_data/";
		setting.replication = 1;
		setting.virtualNodes = 1;
		setting.rankType = 1;
		setting.beaconInterval = 30;
		setting.beaconThreshold = 1;
		setting.backupInterval = 0;
		setting.indexport = Constants.INDEX_PORT;
		setting.searchport = Constants.SEARCH_PORT;
		setting.peerport = Constants.PEER_PORT;
		setting.nodeList = Collections.synchronizedSortedSet(new TreeSet<Node>());
		setting.hashList = new ConcurrentSkipListMap<Long, Node>();
		setting.deleteKeyType = DELTYPE_ID;
		setting.deleteKeyIndex = -1;
		setting.chopper = new BigramChopper();

		return setting;
	}

	/** <i>Tester Method</i> ... This method generates settings for test, with following setting values.
	 * <table>
	 * <tr><th>Setting name</th><th>Value</th></tr>
	 * <tr><td>Block number</td><td>256</td></tr>
	 * <tr><td>Block size</td><td>1048576</td></tr>
	 * <tr><td>directory</td><td>"./orbis_data/"</td></tr>
	 * <tr><td>Replication</td><td>1</td></tr>
	 * </table>
	 * 
	 * @return */
	public static MainServiceSettings initWithTest(){
		MainServiceSettings setting = new MainServiceSettings();
		setting.blockNum = 256;
		setting.blockSize = 1024 * 1024;
		setting.directory = "./orbis_data/";
		setting.replication = 1;
		setting.virtualNodes = 1;
		setting.rankType = 1;
		setting.beaconInterval = 30;
		setting.beaconThreshold = 1;
		setting.backupInterval = 0;
		setting.indexport = Constants.INDEX_PORT;
		setting.searchport = Constants.SEARCH_PORT;
		setting.peerport = Constants.PEER_PORT;
		setting.nodeList = Collections.synchronizedSortedSet(new TreeSet<Node>());
		setting.hashList = new ConcurrentSkipListMap<Long, Node>();
		setting.deleteKeyType = DELTYPE_ID;
		setting.deleteKeyIndex = -1;
		setting.chopper = new WordBigramChopper();
		setting.contentInfo =
			new ContentInfo(new String[]{"Content1", "Content2"}, new String[]{"Appendix1", "Appendix2"},
				new String[]{"Binary1", "binary2"}, new String[]{"Multiple1", "Multiple2"});

		return setting;
	}

	public boolean readSetting(String settingFile){
		boolean isEnded = false;
		// local
		ArrayList<String> contentName = new ArrayList<String>();
		ArrayList<String> appendixName = new ArrayList<String>();
		ArrayList<String> binaryFlagName = new ArrayList<String>();
		ArrayList<String> multipleFlagName = new ArrayList<String>();

		BufferedReader br = null;
		try{
			br = new BufferedReader(new FileReader(new File(settingFile)));
			// properties
			Properties props = new Properties();
			String propval = null;
			props.load(br);

			if((propval = props.getProperty("Directory")) != null){
				if(!propval.endsWith("/")) propval.concat("/");
				if(!(new File(propval).exists())) new File(propval).mkdir();
				this.setDirectory(propval);
				// logger
				LogParser.setParser(Level.ALL.getName(), this.directory());
			}
			if((propval = props.getProperty("BlockSize")) != null){
				this.blockSize = Integer.parseInt(propval);
				if(this.blockSize < 64)
					throw new ArgumentError("Block size must not below 64 bytes, you specified "
						+ this.blockSize);
			}
			if((propval = props.getProperty("ClientLog")) != null)
				if(propval.equalsIgnoreCase("on")) this.clientLog = true;
				else if(propval.equalsIgnoreCase("off")) this.clientLog = false;
			if((propval = props.getProperty("Replication")) != null){
				this.replication = Integer.parseInt(propval);
				if(this.replication < 1)
					throw new ArgumentError("Replicate number must be positive number, you specified "
						+ this.replication);
			}
			if((propval = props.getProperty("IndexPort")) != null){
				this.indexport = Integer.parseInt(propval);
				if(this.indexport < 1 && this.indexport > 65535)
					throw new ArgumentError("Index port must be ranged in 1 - 65535, you specified "
						+ this.indexport);
			}
			if((propval = props.getProperty("SearchPort")) != null){
				this.searchport = Integer.parseInt(propval);
				if(this.searchport < 1 && this.searchport > 65535)
					throw new ArgumentError("Search port must be ranged in 1 - 65535, you specified "
						+ this.searchport);
			}
			if((propval = props.getProperty("PeerPort")) != null){
				this.peerport = Integer.parseInt(propval);
				if(this.peerport < 1 && this.peerport > 65535)
					throw new ArgumentError("Peer port must be ranged in 1 - 65535, you specified "
						+ this.peerport);
			}
			if((propval = props.getProperty("BeaconInterval")) != null){
				this.beaconInterval = Integer.parseInt(propval);
				if(this.beaconInterval < 1)
					throw new ArgumentError("Beacon interval must be positive number, you specified "
						+ this.beaconInterval);
			}
			if((propval = props.getProperty("BeaconThreshold")) != null){
				this.beaconThreshold = Integer.parseInt(propval);
				if(this.beaconThreshold < 1)
					throw new ArgumentError("Beacon threshold must be positive number, you specified "
						+ this.beaconThreshold);
			}
			if((propval = props.getProperty("BackupInterval")) != null){
				this.backupInterval = Integer.parseInt(propval);
				if(this.backupInterval < 0)
					throw new ArgumentError("Backup interval must not be negative number, you specified "
						+ this.backupInterval);
			}
			if((propval = props.getProperty("LogLevel")) != null){
				try{
					LogParser.changeLevel(Level.parse(propval));
				}catch(Exception e){
					LogParser.warning("Specified log-level does not exist.");
				}
			}
			if((propval = props.getProperty("Chopper")) != null){
				propval = new String("org.fujene.term.").concat(propval);
				try{
					@SuppressWarnings("unchecked") Class<Chopper> cls =
						(Class<Chopper>)Class.forName(propval);
					Constructor<Chopper> construct = cls.getConstructor();
					this.setChopper(construct.newInstance());
				}catch(ClassNotFoundException e){
					System.out.println("Class for chopper did not found. application will be shut down.");
				}catch(Exception e){
					throw new InternalError("Internal error.");
				}
			}else this.setChopper(new WordBigramChopper());
			if((propval = props.getProperty("VirtualNode")) != null)
				this.virtualNodes = Integer.valueOf(propval);

			if((propval = props.getProperty("Content")) != null){
				String[] values = propval.split(",");
				for(int i = 0; i < values.length; i++)
					contentName.add(values[i]);
			}
			if((propval = props.getProperty("Appendix")) != null){
				String[] values = propval.split(",");
				for(int i = 0; i < values.length; i++)
					appendixName.add(values[i]);
			}
			if((propval = props.getProperty("binaryFlag")) != null){
				String[] values = propval.split(",");
				for(int i = 0; i < values.length; i++)
					binaryFlagName.add(values[i]);
			}
			if((propval = props.getProperty("multipleFlag")) != null){
				String[] values = propval.split(",");
				for(int i = 0; i < values.length; i++)
					multipleFlagName.add(values[i]);
			}
			if((propval = props.getProperty("Rank")) != null){
				this.setRerank(true);
				if(propval.equalsIgnoreCase("TFIDF")) this.setRankType(Constants.RERANK_TFIDF);
				else if(propval.equalsIgnoreCase("ID")) this.setRankType(Constants.RERANK_ID);
				else this.setRankType(Constants.RERANK_DEFAULT);
			}
			if((propval = props.getProperty("UniqueKey")) != null){
				if(multipleFlagName.contains(propval))
					this.uniqueKeyIndex = multipleFlagName.indexOf(propval);
			}
			if((propval = props.getProperty("DeleteKey")) != null){
				if(contentName.contains(propval)){
					this.deleteKeyType = DELTYPE_CONTENT;
					this.deleteKeyIndex = contentName.indexOf(propval);
				}
				if(appendixName.contains(propval)){
					this.deleteKeyType = DELTYPE_APPENDIX;
					this.deleteKeyIndex = appendixName.indexOf(propval);
				}
				if(multipleFlagName.contains(propval)){
					this.deleteKeyType = DELTYPE_MULTIFLAG;
					this.deleteKeyIndex = multipleFlagName.indexOf(propval);
				}
				this.backupInterval = Integer.parseInt(propval);
			}
			isEnded = true;
		}catch(FileNotFoundException e){
			LogParser.severe("Please input appropriate file. Shutdown.");
			return isEnded;
		}catch(IOException ignored){}finally{
			if(contentName.size() != 0){
				String[] tempArray = new String[0];
				this.setContentInfo(new ContentInfo(contentName.toArray(tempArray), appendixName
					.toArray(tempArray), binaryFlagName.toArray(tempArray), multipleFlagName
					.toArray(tempArray)));
			}
			if(br != null) try{
				br.close();
			}catch(IOException e){}
		}
		return isEnded;
	}

	public int indexThreadNum(){
		return indexThreadNum;
	}

	public void setIndexThreadNum(int indexThreadNum){
		this.indexThreadNum = indexThreadNum;
	}

	public int searchThreadNum(){
		return searchThreadNum;
	}

	public void setSearchThreadNum(int searchThreadNum){
		this.searchThreadNum = searchThreadNum;
	}

	public String directory(){
		return directory;
	}

	public void setDirectory(String directory){
		if(!directory.endsWith("/")) directory.concat("/");
		this.directory = directory;
	}

	public int blockNum(){
		return blockNum;
	}

	public void setBlockNum(int blockNum){
		this.blockNum = blockNum;
	}

	public int blockSize(){
		return blockSize;
	}

	public void setBlockSize(int blockSize){
		this.blockSize = blockSize;
	}

	public int peerport(){
		return peerport;
	}

	public void setPeerport(int peerport){
		this.peerport = peerport;
	}

	public int indexport(){
		return indexport;
	}

	public void setIndexport(int indexport){
		this.indexport = indexport;
	}

	public int searchport(){
		return searchport;
	}

	public void setSearchport(int searchport){
		this.searchport = searchport;
	}

	public int replication(){
		return replication;
	}

	public void setReplication(int replication){
		this.replication = replication;
	}

	public int virtualNodes(){
		return virtualNodes;
	}

	public void setVirtualNodes(int virtualNodes){
		this.virtualNodes = virtualNodes;
	}

	public int rankType(){
		return rankType;
	}

	public void setRankType(int rankType){
		this.rankType = rankType;
	}

	public int beaconInterval(){
		return beaconInterval;
	}

	public void setBeaconInterval(int beaconInterval){
		this.beaconInterval = beaconInterval;
	}

	public int beaconThreshold(){
		return beaconThreshold;
	}

	public void setBeaconThreshold(int beaconThreshold){
		this.beaconThreshold = beaconThreshold;
	}

	public int backupInterval(){
		return backupInterval;
	}

	public void setBackupInterval(int backupInterval){
		this.backupInterval = backupInterval;
	}

	public SortedSet<Node> nodeList(){
		return nodeList;
	}

	public void setNodeList(SortedSet<Node> nodeList){
		this.nodeList = nodeList;
	}

	public void removeNode(Node remNode){
		nodeList.remove(remNode);
	}

	public void addToNodeList(Node newNode){
		nodeList.add(newNode);
	}

	public Node getNode(int index){
		int count = 0;
		for(Node eachNode: nodeList){
			if(count++ == index) return eachNode;
		}

		return null;
	}

	public Node getSameNode(Node nodeNotInList){
		if(nodeList.contains(nodeNotInList)) return nodeList.tailSet(nodeNotInList).first();
		return null;
	}

	public ArrayType getNodeInfo(){
		try{
			MessagePackObject[] infoPack =
				new MessagePackObject[24 + contentInfo.contentsNum() + contentInfo.appendixNum()
					+ contentInfo.binaryFlagNum() + contentInfo.multipleFlagNum()];
			infoPack[0] = IntegerType.create(blockNum);
			infoPack[1] = IntegerType.create(blockSize);
			infoPack[2] = IntegerType.create(replication);
			infoPack[3] = IntegerType.create(0);
			infoPack[4] = IntegerType.create(virtualNodes);
			infoPack[5] = IntegerType.create(beaconInterval);
			infoPack[6] = IntegerType.create(beaconThreshold);
			infoPack[7] = IntegerType.create(backupInterval);
			infoPack[8] = RawType.create(chopper.getClass().getName());
			infoPack[9] = IntegerType.create(0);
			infoPack[10] = IntegerType.create(0);
			infoPack[11] = IntegerType.create(0);
			infoPack[12] = IntegerType.create(LogParser.getLevel().intValue());
			infoPack[13] = IntegerType.create(contentInfo.contentsNum());
			infoPack[14] = IntegerType.create(contentInfo.appendixNum());
			infoPack[15] = IntegerType.create(contentInfo.binaryFlagNum());
			infoPack[16] = IntegerType.create(contentInfo.multipleFlagNum());
			infoPack[17] = IntegerType.create(deleteKeyType);
			infoPack[18] = IntegerType.create(deleteKeyIndex);
			infoPack[19] = IntegerType.create(0);
			infoPack[20] = IntegerType.create(0);
			infoPack[21] = IntegerType.create(0);
			infoPack[22] = IntegerType.create(0);
			infoPack[23] = IntegerType.create(0);
			int index = 24;
			for(int i = 0; i < contentInfo.contentsNum(); i++){
				infoPack[index + i] = RawType.create((contentInfo.contentName(i)));
			}
			index += contentInfo.contentsNum();
			for(int i = 0; i < contentInfo.appendixNum(); i++){
				infoPack[index + i] = RawType.create((contentInfo.appendixName(i)));
			}
			index += contentInfo.appendixNum();
			for(int i = 0; i < contentInfo.binaryFlagNum(); i++){
				infoPack[index + i] = RawType.create((contentInfo.binaryFlagName(i)));
			}
			index += contentInfo.binaryFlagNum();
			for(int i = 0; i < contentInfo.multipleFlagNum(); i++){
				infoPack[index + i] = RawType.create((contentInfo.multipleFlagName(i)));
			}

			ArrayType result = ArrayType.create(infoPack);
			return result;
		}catch(Exception e){
			e.printStackTrace();
		}
		return null;
	}

	public void setNodeInfo(ArrayType infoArray){
		MessagePackObject[] infoObjs = infoArray.asArray();
		this.blockNum = infoObjs[0].asInt();
		this.blockSize = infoObjs[1].asInt();
		this.replication = infoObjs[2].asInt();
		this.virtualNodes = infoObjs[4].asInt();
		this.beaconInterval = infoObjs[5].asInt();
		this.beaconThreshold = infoObjs[6].asInt();
		this.backupInterval = infoObjs[7].asInt();
		String chopperName = infoObjs[8].asString();
		try{
			@SuppressWarnings("unchecked") Class<Chopper> cls = (Class<Chopper>)Class.forName(chopperName);
			Constructor<Chopper> construct = cls.getConstructor();
			this.setChopper(construct.newInstance());
		}catch(ClassNotFoundException e){
			System.out.println("Class for chopper did not found. application will be shut down.");
		}catch(Exception e){
			throw new InternalError("Internal error.");
		}
		LogParser.changeLevel(Level.parse(Integer.toString(infoObjs[12].asInt())));

		this.deleteKeyType = infoObjs[17].asInt();
		this.deleteKeyIndex = infoObjs[18].asInt();
		// content info.
		int contentNum = infoObjs[13].asInt();
		int appendixNum = infoObjs[14].asInt();
		int binaryFlagNum = infoObjs[15].asInt();
		int multipleFlagNum = infoObjs[16].asInt();
		String[] contents = new String[contentNum];
		String[] appendices = new String[appendixNum];
		String[] binaryFlags = new String[binaryFlagNum];
		String[] multiFlags = new String[multipleFlagNum];
		int index = 24;
		for(int i = 0; i < contentNum; i++){
			contents[i] = infoObjs[index + i].asString();
		}
		index += contentNum;
		for(int i = 0; i < appendixNum; i++){
			appendices[i] = infoObjs[index + i].asString();
		}
		index += appendixNum;
		for(int i = 0; i < binaryFlagNum; i++){
			binaryFlags[i] = infoObjs[index + i].asString();
		}
		index += binaryFlagNum;
		for(int i = 0; i < multipleFlagNum; i++){
			multiFlags[i] = infoObjs[index + i].asString();
		}
		this.contentInfo = new ContentInfo(contents, appendices, binaryFlags, multiFlags);

		Node.property(virtualNodes, beaconThreshold);
		myNode.refreshHashTable();
	}

	public int getNodeIndex(Node node){
		if(!nodeList.contains(node)) return -1;
		return nodeList.headSet(node).size();
	}

	public int getNodeAmount(){
		return nodeList.size();
	}

	public boolean isFullCopy(){
		return nodeList.size() <= replication;
	}

	public Node myNode(){
		return myNode;
	}

	public void setMyNode(Node myNode){
		this.myNode = myNode;
		nodeList.add(myNode);
	}

	public String[] firstHost(){
		return firstHost;
	}

	public void setFirstHost(String[] firstHost){
		this.firstHost = firstHost;
	}

	public Chopper chopper(){
		return chopper;
	}

	public void setChopper(Chopper chopper){
		this.chopper = chopper;
	}

	public ContentInfo contentInfo(){
		return contentInfo;
	}

	public void setContentInfo(ContentInfo contentInfo){
		this.contentInfo = contentInfo;
	}

	public ConcurrentSkipListMap<Long, Node> hashList(){
		return hashList;
	}

	public void addToHashList(Long hash, Node node){
		hashList.put(Long.valueOf(hash), node);
	}

	public void removeHashList(Long key){
		hashList.remove(key);
	}

	public Set<Entry<Long, Node>> hashListEntries(){
		return hashList.entrySet();
	}

	public Entry<Long, Node> firstHashEntry(){
		return hashList.firstEntry();
	}

	public Entry<Long, Node> lastHashEntry(){
		return hashList.lastEntry();
	}

	public Entry<Long, Node> ceilingHashEntry(Long from){
		Entry<Long, Node> ceiling = hashList.ceilingEntry(from);
		if(ceiling == null) ceiling = hashList.firstEntry();
		return ceiling;
	}

	public Entry<Long, Node> floorHashEntry(Long from){
		Entry<Long, Node> floor = hashList.floorEntry(from);
		if(floor == null) floor = hashList.lastEntry();
		return floor;
	}

	public Entry<Long, Node> nextHashEntry(Long from){
		Entry<Long, Node> next = hashList.higherEntry(from);
		if(next == null) next = hashList.firstEntry();
		return next;
	}

	// Return value can be null
	public Entry<Long, Node> higherHashEntry(Long from){
		return hashList.higherEntry(from);
	}

	public Entry<Long, Node> prevHashEntry(Long from){
		Entry<Long, Node> prev = hashList.lowerEntry(from);
		if(prev == null) prev = hashList.lastEntry();
		return prev;
	}

	public void resetHashList(){
		this.hashList.clear();
	}

	public int hashListSize(){
		return hashList.size();
	}

	public void setHashList(ConcurrentSkipListMap<Long, Node> hashList){
		this.hashList = hashList;
	}

	public boolean clientLog(){
		return clientLog;
	}

	public void setClientLog(boolean clientLog){
		this.clientLog = clientLog;
	}

	public boolean rerank(){
		return isRerank;
	}

	public void setRerank(boolean isRerank){
		this.isRerank = isRerank;
	}

	public int deleteType(){
		return deleteKeyType;
	}

	public int deleteIndex(){
		return deleteKeyIndex;
	}

	public int uniqueIndex(){
		return uniqueKeyIndex;
	}
}
